﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Threading;
using System.Windows.Threading;
using System.Diagnostics;
using System.Globalization;
using System.Diagnostics.Contracts;
using System.Windows.Automation;
using aut = System.Windows.Automation;

namespace UnitTestUtils.UI.Automation
{
    /// <summary>
    /// Helps hosting <see cref="Control"/>s or <see cref="Window"/>s for testing purposes.
    /// </summary>
    public class UIAutomationHost<TControl> : IDisposable
        where TControl : Control
    {
        readonly string cTestWindowName = "TestWindow_" + Guid.NewGuid().ToString();
        private Window _wnd;
        private TControl _control;

        private AutomationElement _aeWindow;
        private AutomationElement _aeControl;

        private Dispatcher _dispatcher;

        private UIAutomationHost(TControl control, Dispatcher dispatcher)
        {
            Contract.Requires(control != null);
            _control = control;
            _dispatcher = dispatcher;
            hookDispatcherEvents(_dispatcher);

            if (_control is Window)
            {
                _wnd = _control as Window;
            }
            else
            {
                _wnd = new WindowEx();
                _wnd.Name = cTestWindowName;

                //CultureInfo ci = CultureInfo.GetCultureInfo("de-DE");
                //Thread thd = Dispatcher.CurrentDispatcher.Thread;
                //thd.CurrentCulture = ci;
                //thd.CurrentUICulture = ci;

                _wnd.Content = _control;
                
                // set control name to make it accessible via automationID
                if (string.IsNullOrEmpty(_control.Name))
                {
                    _control.Name = "Control_" + Guid.NewGuid().ToString();
                }

                _control.HorizontalAlignment = HorizontalAlignment.Stretch;
                _control.VerticalAlignment = VerticalAlignment.Stretch;
            }
        }

        /// <summary>
        /// Create a UIHost for the passed <see cref="Control"/> or <see cref="Window"/>.
        /// </summary>
        /// <param name="constructorParams">parameters to pass to the constructor</param>
        /// <param name="action">can be used for additional initialization after the control has been created. 
        /// Otherwise pass null.</param>
        /// <returns></returns>
        public static UIAutomationHost<TControl> Create(Action<TControl> action, params object[] constructorParams)
        {
            UIAutomationHost<TControl> host = null;
            string windowName = null;
            string controlName = null;

            var wh = new EventWaitHandle(false, EventResetMode.ManualReset);
            try
            {
                Thread t = new Thread(() =>
                    {
                        var disp = Dispatcher.CurrentDispatcher;
                        TControl control = (TControl)Activator.CreateInstance(typeof(TControl), constructorParams);

                        if (action != null)
                        {
                            action(control);
                        }

                        host = new UIAutomationHost<TControl>(control, disp);

                        windowName = host._wnd.Name;
                        controlName = host._control.Name;
                        host._dispatcher = disp;

                        host._wnd.Loaded += (s, a) =>
                            {
                                wh.Set();
                            };
                        
                        host._wnd.ShowDialog();
                    });
                t.SetApartmentState(ApartmentState.STA);
                Trace.TraceInformation("UIAutomationHost running on thread id:  {0}", t.ManagedThreadId);
                t.Start();

                if (!wh.WaitWithTimeout())
                    throw new TimeoutException("Operation \"Create\" timed out!");

                TreeWalker w = new TreeWalker(ConditionHelper.ByAutomationID(windowName));
                host._aeWindow = w.GetFirstChild(AutomationElement.RootElement);
                
                // check if control equal the window, this means the created control has been a window.
                host._aeControl = (host._wnd == host._control) ? host._aeWindow
                    : host._aeWindow.FindFirst(TreeScope.Children, ConditionHelper.ByAutomationID(controlName));
            }
            finally
            {
                wh.Dispose();
            }
            
            return host;
        }

        /// <summary>
        /// Returns the <see cref="AutomationElement"/> of the hosted Window.
        /// </summary>
        public AutomationElement Window
        {
            get
            {
                return _aeWindow;
            }
        }

        /// <summary>
        /// Returns the <see cref="AutomationElement"/> of the hosted Control.
        /// </summary>
        public AutomationElement Control
        {
            get
            {
                return _aeControl;
            }
        }

        /// <summary>
        /// Returns the native Control without AutomationElement wrapper.
        /// </summary>
        public TControl NativeControl
        {
            get
            {
                return _control;
            }
        }

        /// <summary>
        /// Shows the hosted UI on a modal window.
        /// </summary>
        private void ShowDialog()
        {
            checkDisposed();

            string windowName = null;
            string controlName = null;

            var wh = new EventWaitHandle(false, EventResetMode.ManualReset);

            try
            {
                _dispatcher.BeginInvoke((Action)delegate
                    {
                        checkAndCreateWindow();

                        windowName = _wnd.Name;
                        controlName = _control.Name;
                        wh.Set();

                        _wnd.ShowDialog();
                    });

                if (!wh.WaitWithTimeout())
                    throw new TimeoutException();

                TreeWalker w = new TreeWalker(ConditionHelper.ByAutomationID(windowName));
                _aeWindow = w.GetFirstChild(AutomationElement.RootElement);
                _aeControl = _aeWindow.FindFirst(TreeScope.Children, ConditionHelper.ByAutomationID(controlName));
            }
            finally
            {
                wh.Dispose();
            }
        }

        /// <summary>
        /// Waits until the hosted <see cref="Window"/> is either closed or the default timeout is expired.
        /// </summary>
        public void WaitForClose()
        {
            checkDisposed();
            Dispose();
        }

        /// <summary>
        /// Waits for the user closing the Window.
        /// </summary>
        public void WaitForUserClose()
        {
            checkDisposed();

            EventWaitHandle wh = new EventWaitHandle(false, EventResetMode.ManualReset);
            aut.Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent,
            _aeWindow, TreeScope.Element, (s, a) =>
                 {
                     wh.Set();
                 }
            );
            wh.WaitWithTimeout();
            aut.Automation.RemoveAllEventHandlers();
        }

        /// <summary>
        /// Returns when all messages that currently in the dispatcher queue have been processed.
        /// </summary>
        public void WaitForInputIdle()
        {
            checkDisposed();

            WindowPattern windowPattern = null;
            windowPattern = this.Window.GetCurrentPattern(WindowPattern.Pattern) as WindowPattern;
            
            // Make sure the element is usable.
            if (false == windowPattern.WaitForInputIdle(10000))
            {
                throw new TimeoutException();
            }

            _dispatcher.Invoke((Action)delegate()
            {
            }, DispatcherPriority.ApplicationIdle);
        }

        /// <summary>
        /// Executes action on UI thread the hosted control is bound to.
        /// </summary>
        /// <param name="action">action to perform on UI thread.</param>
        public void Execute(Action<TControl, Window> action)
        {
            checkDisposed();
            _dispatcher.Invoke(action, _control, _wnd);
        }

        #region Internals
        private void checkAndCreateWindow()
        {
            //if (_wnd != null && _wnd.IsLoaded)
            //{
            //    _wnd.Close();
            //    throw new InvalidOperationException("Close previous window before showing a new one!");
            //}

            
        }

        private static void hookDispatcherEvents(Dispatcher disp)
        {
            disp.ShutdownStarted += (s, a) =>
                {
                    Trace.WriteLine("Dispatcher shutdown started");
                };

            disp.ShutdownFinished += (s, a) =>
            {
                Trace.WriteLine("Dispatcher shutdown finished");
            };

            disp.UnhandledException+= (s, a) =>
            {
                Trace.WriteLine("Exception thrown on Dispatcher thread:\r\n" + a.Exception.ToString() );
            };
        }

        class WindowEx : Window
        {
            public WindowEx()
            {
                Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
            }
        } 
        #endregion

        #region IDisposable Members
        private void checkDisposed()
        {
            if (_dispatcher == null)
            {
                throw new ObjectDisposedException("UIAutomationHost");
            }
        }

        /// <summary>
        /// Performs internal cleanup.
        /// </summary>
        public void Dispose()
        {
            if (_dispatcher != null)
            {
                var tempDispatcher = _dispatcher;
                _dispatcher.BeginInvoke( (Action)delegate
                    {
                        _wnd.Close();
                        Trace.WriteLine("Window closed");

                        tempDispatcher.BeginInvokeShutdown(DispatcherPriority.Normal);
                        Trace.WriteLine("Dispatcher shutdown");
                    });
                // dispatcher is shutdown already (see create method)
                _dispatcher = null;
            }
        }

        #endregion
    }
}
